package brs.assetexchange;

import brs.*;
import brs.Order.Ask;
import brs.Order.Bid;
import brs.db.BurstKey;
import brs.db.BurstKey.LongKeyFactory;
import brs.db.VersionedEntityTable;
import brs.db.store.OrderStore;
import brs.services.AccountService;
import brs.util.Convert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;

class OrderServiceImpl {
  private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);
  private final OrderStore orderStore;
  private final VersionedEntityTable<Ask> askOrderTable;
  private final LongKeyFactory<Ask> askOrderDbKeyFactory;
  private final VersionedEntityTable<Bid> bidOrderTable;
  private final LongKeyFactory<Bid> bidOrderDbKeyFactory;
  private final AccountService accountService;
  private final TradeServiceImpl tradeService;
  private final AssetServiceImpl assetService;

  public OrderServiceImpl(OrderStore orderStore, AccountService accountService, TradeServiceImpl tradeService,AssetServiceImpl assetService) {
    this.orderStore = orderStore;
    this.askOrderTable = orderStore.getAskOrderTable();
    this.askOrderDbKeyFactory = orderStore.getAskOrderDbKeyFactory();
    this.bidOrderTable = orderStore.getBidOrderTable();
    this.bidOrderDbKeyFactory = orderStore.getBidOrderDbKeyFactory();

    this.assetService = assetService;
    this.accountService = accountService;
    this.tradeService = tradeService;
  }

  public Ask getAskOrder(long orderId) {
    Ask ask = askOrderTable.get(askOrderDbKeyFactory.newKey(orderId));
    Map<Long,Long> orderId2FillQuantity = orderStore.getFillQuantityByAskOrder(Arrays.asList(ask.getId()));
    if (orderId2FillQuantity.get(ask.getId()) != null) {
      ask.setFillQuantityQNT(orderId2FillQuantity.get(ask.getId()));
    }

    return ask;
  }

  public Bid getBidOrder(long orderId) {
    Bid bid = bidOrderTable.get(bidOrderDbKeyFactory.newKey(orderId));
    Map<Long,Long> orderId2FillQuantity = orderStore.getFillQuantityByBidOrder(Arrays.asList(bid.getId()));
    if (orderId2FillQuantity.get(bid.getId()) != null) {
      bid.setFillQuantityQNT(orderId2FillQuantity.get(bid.getId()));
    }

    return bid;
  }

  public Collection<Ask> getAllAskOrders(int from, int to) {
    return askOrderTable.getAll(from, to);
  }

  public Collection<Bid> getAllBidOrders(int from, int to) {
    return bidOrderTable.getAll(from, to);
  }

  public Collection<Bid> getSortedBidOrders(long assetId, int from, int to) {
    return orderStore.getSortedBids(assetId, from, to);
  }


  public Collection<Bid> getSortedBidOrders(long assetId, long quoteAssetId, int from, int to) {
    return orderStore.getSortedBids(assetId, quoteAssetId, from, to);
  }

  public Collection<Ask> getAskOrdersByAccount(long accountId, int from, int to) {
    return orderStore.getAskOrdersByAccount(accountId, from, to);
  }

  public Collection<Ask> getAskOrdersByAccountAsset(final long accountId, final long assetId, int from, int to) {
    return orderStore.getAskOrdersByAccountAsset(accountId, assetId, from, to);
  }

  public Collection<Ask> getAskOrdersByAccountAsset(final long accountId, final long assetId, long quoteAssetId, int from, int to) {
    return orderStore.getAskOrdersByAccountAsset(accountId, assetId, quoteAssetId, from, to);
  }

  public Collection<Ask> getSortedAskOrders(long assetId, int from, int to) {
    return orderStore.getSortedAsks(assetId, from, to);
  }

  public Collection<Ask> getSortedAskOrders(long assetId, long quoteAssetId, int from, int to) {
    return orderStore.getSortedAsks(assetId, quoteAssetId, from, to);
  }

  public int getBidCount() {
    return bidOrderTable.getCount();
  }

  public int getAskCount() {
    return askOrderTable.getCount();
  }

  public Collection<Bid> getBidOrdersByAccount(long accountId, int from, int to) {
    return orderStore.getBidOrdersByAccount(accountId, from, to);
  }

  public Collection<Bid> getBidOrdersByAccountAsset(final long accountId, final long assetId, int from, int to) {
    return orderStore.getBidOrdersByAccountAsset(accountId, assetId, from, to);
  }


  public Collection<Bid> getBidOrdersByAccountAsset(final long accountId, final long assetId, long quoteAssetId, int from, int to) {
    return orderStore.getBidOrdersByAccountAsset(accountId, assetId, quoteAssetId, from, to);
  }

  public void removeBidOrder(long orderId) {
    bidOrderTable.delete(getBidOrder(orderId));
  }

  public void removeAskOrder(long orderId) {
    askOrderTable.delete(getAskOrder(orderId));
  }

  public void addAskOrder(Transaction transaction, Attachment.ColoredCoinsAskOrderPlacement attachment) {
    BurstKey dbKey = askOrderDbKeyFactory.newKey(transaction.getId());
    Ask order = new Ask(dbKey, transaction, attachment);
    askOrderTable.insert(order);
    matchOrders(attachment.getAssetId(), attachment.getQuoteAssetId());
  }

  public void addBidOrder(Transaction transaction, Attachment.ColoredCoinsBidOrderPlacement attachment) {
    BurstKey dbKey = bidOrderDbKeyFactory.newKey(transaction.getId());
    Bid order = new Bid(dbKey, transaction, attachment);
    bidOrderTable.insert(order);
    matchOrders(attachment.getAssetId(), attachment.getQuoteAssetId());
  }

  private Ask getNextAskOrder(long assetId, long quoteAssetId) {
    return Burst.getStores().getOrderStore().getNextOrder(assetId,quoteAssetId);
  }

  private Bid getNextBidOrder(long assetId, long quoteAssetId) {
    return Burst.getStores().getOrderStore().getNextBid(assetId,quoteAssetId);
  }

  private long getAmount(byte assetDecimals,  long quantityNQT, long priceNQT) {
       long exp = (long)Math.pow(10, assetDecimals);

       return new BigDecimal(quantityNQT).multiply(new BigDecimal(priceNQT)).divide(new BigDecimal(exp), 0, RoundingMode.HALF_UP).longValue();
  }

  private void matchOrders(long assetId, long quoteAssetId) {

    Order.Ask askOrder;
    Order.Bid bidOrder;

    while ((askOrder = getNextAskOrder(assetId,quoteAssetId)) != null
        && (bidOrder = getNextBidOrder(assetId, quoteAssetId)) != null) {
       //卖单价格比买单价格高，不可能成交，直接退出，因为买一和卖一不能成交，后面的订单就更不可能成交
      if (askOrder.getPriceNQT() > bidOrder.getPriceNQT()) {
        break;
      }

      //同一个用户的卖单和买单符合交易条件，也成交，记录日志
      if (askOrder.getAccountId() == bidOrder.getAccountId()) {
        logger.error("bid order:{} and ask order {} matched, user ={} ", bidOrder.getId(), askOrder.getId(), askOrder.getAccountId());
      }

      Asset asset = assetService.getAsset(assetId);
      byte quoteDecimals = 8;
      if (askOrder.getQuoteAssetId() != 0) {
         //所有资产的精度都是6
         quoteDecimals = 6;
      }

      byte assetDecimals = asset.getDecimals();
      //成交的价格是以先下的订单价格，比如系统中有一笔价格10的买单，接下来有一笔价格为1的卖单，那么成交价是10
      Trade trade = tradeService.addTrade(assetId, Burst.getBlockchain().getLastBlock(), askOrder, bidOrder,quoteDecimals, askOrder.getQuoteAssetId());
      //更新卖单的剩余数量
      askOrderUpdateQuantityQNT(askOrder, Convert.safeSubtract(askOrder.getQuantityQNT(), trade.getQuantityQNT()));
      Account askAccount = accountService.getAccount(askOrder.getAccountId());
      //卖方 增加 价格资产
      if (quoteAssetId == 0) {
        //用BURST计价
        accountService.addToBalanceAndUnconfirmedBalanceNQT(askAccount, getAmount(assetDecimals,trade.getQuantityQNT(), trade.getPriceNQT()));
      } else {
        accountService.addToAssetAndUnconfirmedAssetBalanceQNT(askAccount, quoteAssetId,  getAmount(assetDecimals,trade.getQuantityQNT(), trade.getPriceNQT()));
      }

      //卖方减少 资产 -- 注意：挂卖单时只从unconfirm里扣除了资产，所以成交后就不需要再从unconfirm扣除了
      accountService.addToAssetBalanceQNT(askAccount, assetId, -trade.getQuantityQNT());

      //更新买单的剩余数量
      bidOrderUpdateQuantityQNT(bidOrder, Convert.safeSubtract(bidOrder.getQuantityQNT(), trade.getQuantityQNT()));
      Account bidAccount = accountService.getAccount(bidOrder.getAccountId());
      //买方 增加资产
      accountService.addToAssetAndUnconfirmedAssetBalanceQNT(bidAccount, assetId, trade.getQuantityQNT());

      if (quoteAssetId == 0) {
        //买方 减少 价格资产
        accountService.addToBalanceNQT(bidAccount, -getAmount(assetDecimals,trade.getQuantityQNT(), trade.getPriceNQT()));
        //成交价格可能比买单低，要把差价部分返还给买家 (注意扣除是从unconfirm里扣除的，所以返还也返还到unconfirm里)
        accountService.addToUnconfirmedBalanceNQT(bidAccount, getAmount(assetDecimals,trade.getQuantityQNT(), (bidOrder.getPriceNQT() - trade.getPriceNQT())));
      } else {
        accountService.addToAssetBalanceQNT(bidAccount, quoteAssetId,  -getAmount(assetDecimals,trade.getQuantityQNT(), trade.getPriceNQT()));
        accountService.addToUnconfirmedAssetBalanceQNT(bidAccount, quoteAssetId,  getAmount(assetDecimals,trade.getQuantityQNT(), (bidOrder.getPriceNQT() - trade.getPriceNQT())));
      }

      logTrade(trade);
    }
  }

  private void logTrade(Trade trade) {
     logger.debug("order matched. ask order: {}, bid order:{},");
  }

  private void askOrderUpdateQuantityQNT(Ask askOrder, long quantityQNT) {
    askOrder.setQuantityQNT(quantityQNT);
    if (quantityQNT > 0) {
      askOrderTable.insert(askOrder);
    } else if (quantityQNT == 0) {
      askOrderTable.delete(askOrder);
    } else {
      throw new IllegalArgumentException("Negative quantity: " + quantityQNT
          + " for order: " + Convert.toUnsignedLong(askOrder.getId()));
    }
  }

  private void bidOrderUpdateQuantityQNT(Bid bidOrder, long quantityQNT) {
    bidOrder.setQuantityQNT(quantityQNT);
    if (quantityQNT > 0) {
      bidOrderTable.insert(bidOrder);
    } else if (quantityQNT == 0) {
      bidOrderTable.delete(bidOrder);
    } else {
      throw new IllegalArgumentException("Negative quantity: " + quantityQNT
          + " for order: " + Convert.toUnsignedLong(bidOrder.getId()));
    }
  }


}
